package com.qcby.domain.core.handler;

import cn.hutool.core.collection.CollectionUtil;

import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.baomidou.mybatisplus.core.toolkit.Assert;
import com.baomidou.mybatisplus.core.toolkit.EncryptUtils;
import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import lombok.extern.slf4j.Slf4j;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;

import java.sql.Connection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 自定义 SQL 性能规范拦截器
 * <p>
 * 拦截 SQL 类型的场景
 * 1. 必须使用到索引，包含 left join 连接字段，符合索引最左原则
 * 必须使用索引好处：
 * 1.1 如果因为动态 SQL，bug 导致 update 的 where 条件没有带上，全表更新上万条数据
 * 1.2 如果检查到使用了索引，SQL 性能基本不会太差
 * <p>
 * 2. SQL 单表执行，禁止任何 left join，如果不能拆成单表执行的 SQL，请 RD -> 三级 leader -> 二级 leader 被 TC 挑战后再决定。
 * SQL 尽量单表执行的好处：
 * 2.1 查询条件简单、易于开理解和维护；
 * 2.2 扩展性极强；（可为分库分表做准备）
 * 2.3 缓存利用率高；
 * <p>
 * 3. 在字段上使用函数
 * 4. where 条件为空
 * 5. where 条件使用了 !=
 * 6. where 条件使用了 not 关键字（升级 jsqlparser 后待实现）
 * 7. where 条件使用了 or 关键字
 * 8. where 条件使用了 使用子查询
 *
 * @author: cong.zhen
 * @date: 2022/11/10
 */
@Slf4j
public class IllegalSqlInnerInterceptor extends JsqlParserSupport implements InnerInterceptor {
    /**
     * 忽略某些特殊 SQL
     */
    private static final Set<String> IGNORE_SQL_SET =
           CollectionUtil.newHashSet("SELECT LAST_INSERT_ID()", "SELECT LAST_INSERT_ID() from dual where 1=1", "SELECT LAST_INSERT_ID() FROM DUAL WHERE 1=1");

    /**
     * 缓存验证结果，提高性能
     */
    private static final Set<String> CACHE_VALID_RESULT = new HashSet<>();



    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        PluginUtils.MPStatementHandler mpStatementHandler = PluginUtils.mpStatementHandler(sh);
        MappedStatement ms = mpStatementHandler.mappedStatement();
        SqlCommandType sct = ms.getSqlCommandType();

        if (sct == SqlCommandType.INSERT
                || InterceptorIgnoreHelper.willIgnoreIllegalSql(ms.getId())
        ) {
            return;
        }

        BoundSql boundSql = mpStatementHandler.boundSql();
        String originalSql = boundSql.getSql();
        if (IGNORE_SQL_SET.contains(originalSql.trim())) {
            logger.debug("该SQL已配置忽略，无需验证，SQL:" + originalSql);
            return;
        }
        String md5Base64 = EncryptUtils.md5Base64(originalSql);
        if (CACHE_VALID_RESULT.contains(md5Base64)) {
            logger.debug("该SQL已验证通过，无需再次验证，SQL:" + originalSql);
            return;
        }

        logger.debug("检查SQL是否合规，SQL:" + originalSql);
        parserSingle(originalSql, connection);
        //缓存验证结果
        CACHE_VALID_RESULT.add(md5Base64);
    }

    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        PlainSelect plainSelect = (PlainSelect) select.getSelectBody();
        Expression where = plainSelect.getWhere();
        Assert.notNull(where, "非法SQL，必须要有where条件");
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        Expression where = update.getWhere();
        Assert.notNull(where, "非法SQL，必须要有where条件");
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        Expression where = delete.getWhere();
        Assert.notNull(where, "非法SQL，必须要有where条件");
    }
}
